这篇注重实战,主要内容包括:
- MySQL 慢查询分析相关资料
- 常见的慢查询分析思路
- PROD 遇到的 CASE 积累
EXPLAIN 常见资料
- 官方文档系列
- Explain output
- EXPLAIN syntax
- EXPLAIN EXTENDED output
- 优化器相关
- 写好查询的一些建议
- 慢查询相关的配置
- 介绍索引
- 常见的工具
- pt-slowlog:比MySQL官方的慢查询信息要丰富,详情可以戳这里
几个关键点:
- 5.7版本开始,支持的 DML 语句类型更多了(比如 UPDATE 什么的)
- EXPLAIN EXTENDED 只有 SELECT 才有,5.7 默认开启(即不用显示
EXPLAIN EXTENDED
)
常见的慢查询分析思路
撸了一些文档+文章后,不总结一些自己的套路,总会存在记不住的感觉。这里总结了
问题 SQL 的定位
通常是根据业务场景,设置合理的 long_query_time
& slow_query_log=ON
来发现慢查询。当然有一些工具来分析,上面有提及。通常在筛选慢查询的时候,考虑:
- order by 慢查询次数,选取 top xx
- order by 慢查询 avg time,选取 top xx
- order by tp95 慢查询,选取 top xx
EXPLAIN 分析具体语句
问题到了这里通常是已经定位了具体的 SQL,然后需要分析原因了。常见的问题一般是:
- 没有用到预期的索引
- 没有用索引
- 优化器选择了错误索引
- 索引没有实现覆盖索引
- 扫描的行数过多
- 查询没什么问题,IO 有问题
- 返回无用列,且无用列有明显 IO 性能开销(比如TEXT or BLOB or JSON 之类的)
索引分析
使用了哪个索引,以及使用了索引的哪一部分:
- 通常看
possible_keys
以及key
两个字段 possible_keys
的内容取决于where
语句 +EXPLAIN TABLE
的结果,即where
会用到哪些EXPLAIN TABLE
中能看到的索引- TODO:补充下possible_keys选择index的条件
key
表示 MySQL 存储引擎实际选择的索引。这个索引可能不是possible_keys
中列出来的key_len
表示实际使用的索引的长度,通常能根据这个字段 +key
来判断用了key
的哪几个字段
当前表的关联方式,也对查询性能影响较大。对应的在 EXPLAIN 结果中,有个 type
字段来参考关联方式。通常问题 SQL 中会出现 type=ALL
或者 type=index
的情况,即全表扫描。下面列举了下 type
的可能值,按照性能从好到差来排序。
- system & const: 表示你条件可以被转换为常数列(即能通过条件确定唯一值),比如
- 只有一行记录的系统表:system
- where 条件是 pk or unique key 的唯一值,例如
a=1 & a is pk or uk
- eq_ref & ref & ref_or_null
- eq_ref:previous table 的内容能在当前 table 唯一确认一列
- ref:用了索引,但是做不到 eq_ref 那么好,就是 ref
- ref_or_null:等价 ref,比 ref 多比较了一个 null 的情况
- fulltext:使用了
FULLTEXT
索引 - index_merge
- 采用了多个索引,此时
key
列会展示用到的具体索引
- 采用了多个索引,此时
- unique_subquery & index_subquery
- 针对 IN 类型的 subquery 优化
- range
- 用了索引,判断采用范围读取的方式比较好
- 通常会出现在范围类型的 operator & 当前索引中用到的列,其区分度比较小的情况
range
对 column 相关 operator 是有要求的,支持的有:=, <>, >, >=, <, <=, IS NULL, <=>, BETWEEN, or IN()
- index & all
- 都是全表扫描,区别是用非主键还是主键
- 要注意的是,不同索引的扫描,数据的返回顺序是不一样的
关于 eq_ref & ref 的理解,其实是 MySQL 在拿到 previous 表的数据,要处理本表的时候,想偷懒。那什么情况能偷懒呢?当本表的关联字段能用到 uk or pk 的时候,MySQL 知道比一次就 ok了。如果没法确认唯一值,则用的就是ref。ref_or_null 是 ref 的一个特殊情况,针对可空的列的比较。
IO 分析
通常可以通过 EXPLAIN
给出的行数预估来分析大概的 IO 开销,具体的字段有
- rows
- MySQL 认为执行当前查询需要遍历的行数
- 在 innodb 中,这是个估计值;TODO 补充下 why
- filter:表示实际有效的rows / MySQL scan 的 rows,这个值在 1-100 之间,一般越大越好
- stackoverflow:filter means
- 严格意义上
filter
代表的内容实际已经在 IO 发生之后,但是filter
参数可以体现你的查询中无效 IO 的占比
pt-slow-query 一些参数
除了通过 EXPLAIN
来分析 MySQL 执行语句的行为之外,还可以通过一些额外的参数来分析。percona
在代码中进行了埋点,可以对慢查询进行监测。常见的指标解释如下:
- Query_time:查询时间
- Lock_time:锁时间 TODO 持有锁吧?
- Rows_sent:实际返回的 row 数量
- Rows_examined:实际读取的 row 数量
- Rows_affected:查询影响的行数量(UPDATE 类似的)
- Bytes_sent:Rows_sent * query row size
- Tmp_tables:查询中,在内存中创建的临时表数量
- Tmp_disk_tables:查询中,在 disk 上创建的临时表数量
- Tmp_table_sizes:查询用到的临时表(mem+disk)总大小
- QC_Hit:缓存命中
- Full_scan:是否全表扫描
- Full_join:是否 join without index
- Tmp_table:是否用到临时表;yes or no
- Tmp_table_on_disk:查询是否创建 disk 临时表;yes or no
- Filesort:查询是否用到的 filesort;yes or no
- Filesort_on_disk:查询是否用到 disk filesort ;yes or no
- Merge_passes:TODO 暂时不知道
- InnoDB_IO_r_ops:当前查询 read page 操作的次数(一个估计值)
- InnoDB_IO_r_bytes:read page * page size
- InnoDB_IO_r_wait:查询花费在 io 设备上等待上的时间
- InnoDB_rec_lock_wait:查询花费在等待 row lock 的时间; TODO table lock 呢?
- InnoDB_queue_wait:当前查询等待被调度的时间;TODO 看看是不是跟 cpu 有关呢
- InnoDB_pages_distinct:统计本次查询 read page 去重后的数量
- 这个是依赖于一个 hash_array 去计算的,比 InnoDB_IO_r_ops 还不准
- btw,数越大越不准
通常分析的时候,会考虑:
- 看看有没有问题项,比如:Full_scan,Full_join,Tmp_table,Tmp_table_on_disk
- 看看查询的 IO 开销,如:InnoDB_IO_r_wait,InnoDB_IO_r_ops,
- 看看是不是锁导致的:InnoDB_IO_lock_wait
- 看看是不是 cpu 负载高导致的:InnoDB_queue_wait
官方文档在这里。
需要注意的是,log_slow_verbosity
这个参数对具体输出的内容影响还是比较大的。这个参数介绍如下:
- 能指定 slow log 中到底输出什么玩意儿
- 可配置的内容有:
- microtime:是否用微秒计算查询
- query_plan:是否输出 EXPLAIN 的内容
- innodb:是否输出 innodb 的一些静态参数
- minimal:等价于 microtime
- standard:等价于 microtime,innodb
- full:等价于 microtime, query_plan, innodb
- profiling:开启性能剖析
- profiling_use_getrusage:开启高级性能分析
通常我们的配置是 full
另外一些相关的参数有:
- log_slow_filter:慢查询采样的方式,query 级别或者 session 级别;
- log_slow_rate_limit:采样的比率,1-100,默认是1,记录全量;跟上面的参数结合使用
- log_slow_sp_statements:是不是记录 stored procedures 的慢日志
- slow_query_log_use_global_control:慢查询的参数是用全局的还是local的啊
- slow_query_log_always_write_time:定义了日志在查询结束后多久开始写
通常是记录 query 级别的慢查询,并且记录全量的。
MySQL INFORMATION_SCHEMA
TODO:待补充
优化成本与收益的评估
CASE 分析
这里整理了一些典型的 CASE 进行分析
大查询
|
|
explain
|
|
explain 没有什么问题。继续看慢查询日志,能发现:
|
|
问题应该比较明显,花在 IO 上的时间太多;IO 花费高的原因从 InnoDB_pages_distinct 能看出,
update demo
|
|
看下 EXPLAIN:
|
|
慢查询的信息:
|
|
能看到的慢查询中可疑部分就是:
|
|
explain
|
|
在 EXPLAIN 中就能看到,对应问题是:
- rows 过多,且 filter 比较小
找其他参数验证下:
|
|
主要开销确定是由于读取的行数过多导致的,建议增加limit采用分页的方式来完成该查询。
an delete demo
|
|
对应 EXPLAIN
|
|
很有意思,跟之前 INSERT 的 range = ALL
有区别;并且这个 SQL 出现的时间点,跟 DB 的写高峰压力基本差不多,且 InnoDB_IO_r_wait
参数也没有明显的问题。
TODO:待补充原因
EXPLAIN 中的错误信息
|
|
|
|
table-sql
|
|
查询的慢日志:
|
|
看慢日志可知,基本是一个全表遍历,最后因为没有 match,而没有实际发送数据。更可怕的是InnoDB_pages_distinct
到了 2678!虽然这不是个准确值但是表示读取的 pages 也很多了。按照这个 SQL 去分析,执行计划其实没错,只不过根据 limit 错误的估计了 rows 的数量。一定要通过 HASH 去比较的话,就要增加针对 hashcode 字段的索引。
扩展阅读
- MySQL 支持 INSERT / UPDATE / DELETE 的 EXPLAIN 查看
- 更详细的内容可以在 git log 中搜索 WL#4897
- 其中主 commit 为:326cf3e3c455664aa06ca65ffcdfc5d1ed3dff33
- MariaDB内核开发Sergey Petrunia’s blog